package hichecker.ui.components.custom_table;

import hichecker.tools.export.CSVFile;
import hichecker.university.Course;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

import javax.swing.event.TableModelEvent;
import javax.swing.table.AbstractTableModel;

public abstract class AbstractCustomTableModel<T> extends AbstractTableModel  {
	/* DECLARATION ****************************************************************************** */
	/**
	 * Kopie des dataVector, indem in einer HashMap zu den ZeilenIDs die jeweiligen Werte
	 * aller Spalten gespeichert werden. Die ZeilenIDs werden durch die Methode
	 * {@link #getRowID} ermittelt. Wird erst durch ein Update verändert.
	 */
	protected HashMap<T, List<Object>> defaultVector = new HashMap<T, List<Object>>();

	/**
	 * Gibt an, welche Spalten beim Überprüfen auf Änderungen ignoriert werden sollen. <br>
	 * Wenn das Array auf {@code null} gesetzt wird, werden alle Spalten ignoriert <br>
	 * Wird es hingegen auf ein Array der Länge 0 gesetzt, werden alle Spalten berücksichtigt <br>
	 *
	 * @see #cellHasChanged für weitere Informationen, wann die
	 *      Änderungen in einer Zelle ignoriert werden
	 */
	protected boolean[] ignoredColumns;

	protected Object[] headerValues;

	/* FUNCTION ******************************************************************************** */
	public void updateDefaultData() {
		defaultVector = new HashMap<T, List<Object>>();
		for (int i = 0; i < getRowCount(); i++) {
			T id = getRowID(i);
			List<Object> values = new ArrayList<Object>();
			for (int j = 0; j < getColumnCount(); j++) {
				values.add(getValueAt(i, j));
			}
			defaultVector.put(id, values);
		}
	}

	/**
	 * Setzt den momentan in einer Zelle anliegend Wert als Standardwert
	 */
	public void setAsDefault(int row, int column) {
		Object value = getValueAt(row, column);
		defaultVector.get(row).set(column, value);
	}

	/**
	 * Prüft, ob das TableModel Änderungen aufweist. Dies ist der Fall, wenn die Anzahl der Zeilen
	 * verändert hat, oder aber ein Wert in einer Zelle sich geändert hat.
	 *
	 * @see #cellHasChanged für Informationen, wann die Änderungen in einer Zelle
	 *      ignoriert werden
	 */
	public boolean hasBeenModified() {
		if (getRowCount() != defaultVector.keySet().size())
			return true;

		boolean changed = false;
		for (int row = 0; row < getRowCount(); row++)
			changed = changed || cellsHaveChanged(row, TableModelEvent.ALL_COLUMNS);

		return changed;
	}

	/**
	 * Prüft ob eine Menge von Zellen veränderte Werte haben.
	 *
	 * @param row
	 *            Zeile der Spalte
	 * @param column
	 *            um alle Spalten eine Reihe zu überprüfen muss der Wert
	 *            {@link TableModelEvent#ALL_COLUMNS} gesetzt werden. Ansonsten wird nur die
	 *            übergebene Spalte geprüft
	 * @see #cellHasChanged für Informationen, wann die
	 *      Änderungen in einer Zelle ignoriert werden
	 */
	public boolean cellsHaveChanged(int row, int column) {
		// Ein Spalte wurde verändert
		if (column != TableModelEvent.ALL_COLUMNS) {
			return cellHasChanged(row, column);
		}

		// Alle Spalten wurden verändert, jede einzelne prüfen
		boolean changed = false;
		for (int i = 0; i < getColumnCount(); i++) {
			if (cellHasChanged(row, i))
				changed = true;
		}

		return changed;
	}

	/**
	 * Prüft ob eine Zelle verändert wurde. Die geschieht indem der momentan anliegende Wert mit dem
	 * Wert im {@link #defaultVector} verglichen wird. <br>
	 * Abhängig vom gesetzten Wert für {@link #ignoredColumns} gibt es Ausnahmen, in denen die
	 * Änderungen in der Zelle ignoriert wird: </br> <b>(a) </b> {@code ignoredColumns == null}
	 * </br> <b>(b) </b> {@code ignoredColumns.length > column} aber {@code ignoredColumns.length
	 * != 0} </br> <b>(c) </b> {@code ignoredColumns[columns] == false} </br>
	 *
	 * @param row Zeile
	 * @param column Spalte
	 */
	public boolean cellHasChanged(int row, int column) {

		if (!defaultVector.containsKey(getRowID(row)))
			return true;

		// Prüfen, ob Zelle ignoriert wird
		if (ignoredColumns == null || (ignoredColumns.length != 0 && ignoredColumns.length > column && ignoredColumns[column]))
			return false;

		Object defaultValue = getDefaultValue(row, column);
		if (defaultValue == null)
			return true;
		if (!getValueAt(row, column).equals(defaultValue))
			return true;
		return false;
	}

	/* GETTER+SETTER *************************************************************************** */
	/**
	 * Liefert den Wert der entsprechenden Zelle
	 */
	@Override
	public abstract Object getValueAt(int row, int column);

	/**
	 * Liefert den als Standard gesetzten Wert der entsprechende Zelle
	 *
	 * @return <b>{@code null}</b>, falls kein Eintrag für die ZeilenID existiert oder die Anzahl der gespeichert
	 *         Einträge für die Zeile kleiner als <i>column</i> ist
	 */
	private Object getDefaultValue(int row, int column) {
		T id = getRowID(row);
		if (defaultVector == null)
			defaultVector = new HashMap<T, List<Object>>();
		if (defaultVector.get(id) == null)
			return null;
		if (defaultVector.get(id).size() < column)
			return null;
		return defaultVector.get(id).get(column);
	}

	/**
	 * Gibt für eine Zeile die entsprechende ID zurück. Je nachdem, um was es sich dabei für Objekte
	 * in den Tabellen handelt kann dies etwa die {@link series#id} oder die
	 * {@link Course#id} sein <br>
	 * Muss für jede erbende Klasse extra implementiert werden
	 */
	abstract protected T getRowID(int row);

	/**
	 * Returns the values of all headers. May be any kind of object. Supports multilinig by
	 * returning an array of objects for a column.
	 */
	abstract public Object[] getHeaderValues();

	public CSVFile toCSV(String name) {
		CSVFile file = new CSVFile(name, getColumnCount());

		String[] header = new String[getColumnCount()];
		for (int h = 0; h < getColumnCount(); h++)
			header[h] = headerValues[h].toString().replaceAll("</?[a-zA-Z_0-9]+>", "");
		file.addLine(header);

		for (int r = 0; r < getRowCount(); r++) {
			String[] values = new String[getColumnCount()];
			for (int c = 0; c < getColumnCount(); c++) {
				Object value = getValueAt(r, c);
				values[c] = value == null ? "" : value.toString();
			}
			file.addLine(values);
		}

		return file;
	}
}